<?php
class Validator extends IError
{
    /** Validation fields configuration
     * @var array
     */
    var $fields = array();

    /** Validation rules
     * @var array
     */
    var $rules = array();  // 0 - first op, 1 - second op, 2 - operation


    /** Callback function for whole form rules
     * @var array
     */
    var $callback = array();


    /** Constuctor.
     * @param array $fields array of field validation schemes
     * @param array $rules validation rules
     */
    function Validator($fields, $rules=array(), $callback=array() )
    {
        // very important (!) for debug
        $aKeys = array('def', 'title', 'pattern', 'optional', 'minlen', 'maxlen', 'min', 'max', 'mineq', 'maxeq', 'callback', 'message', 'maxsize', 'minsize');
        foreach ($fields as $k => $scheme)
        {
            if (isset($scheme['def']))
            {
                $scheme = array_merge($this->_getDef($scheme['def'], $scheme['title']), $scheme);
                unset($scheme['def']);
            }

            if (array_diff(array_keys($scheme), $aKeys))
                IError::fatal('validator.unknown_keys');

            $this->fields[$k] = $scheme;
        }
        $this->rules    = $rules;
        $this->callback = $callback;
    }

    /** Returns specified value from a given array. supports one [] in names
     * @param string $k    name of variable
     * @param string $values values array (_POST)
     * @access private
     * @return string
     */
    function _getValue($k,$values)
    {
        // vitally important for debug ( do not use isset (!) )
        $val = '';
        if ( preg_match('/^(.+)\[(.+)\]$/', $k, $mat) )
            $val = $values[$mat[1]][$mat[2]];
        elseif ( preg_match('/^(.+)\[\]$/', $k, $mat) )
            $val = isset($values[$mat[1]])? $values[$mat[1]] : array();
        else
            $val = (array_key_exists($k, $values)) ? $values[$k] : "";

        return $val;
    }

    /** Checks whether data is valid according to rules
     *
     * @param array $values values to check
     * @return boolean true - values are valid, false - invalid values
     */
    function isValid($values)
    {
       //single fields
        foreach ($this->fields as $field => $scheme)
            $this->_checkField($this->_getValue($field, $values), $scheme);

        //rules
        foreach ($this->rules as $rule)
            $this->_checkRule($rule, $values);

        //callback
        foreach ($this->callback as $callback)
        {
            $err = call_user_func($callback[0], $values);
            $this->addErrors($err);
        }
        return (0 == sizeof($this->_errors));
    }


    function _checkRule($rule, $values)
    {
        $op1 = $this->_getValue($rule[0], $values);
        $op2 = $this->_getValue($rule[1], $values);
        $isError = false;
        switch ($rule[2])
        {
            case '==':
                $isError = !($op1 == $op2);
            break;
            case '!=':
                $isError = !($op1 != $op2);
            break;
            case '<=':
                $isError = (($op1!='') && ($op2!='') && !($op1 <= $op2));
            break;
            case '>=':
                $isError = (($op1!='') && ($op2!='') && !($op1 >= $op2));
            break;
            case 'req':
                $isError = ($op1 and !$op2);
            break;

            case '>':
                $isError = (($op1!='') && ($op2!='') && !($op1 > $op2));
            break;
            case '<':
                $isError = (($op1!='') && ($op2!='') && !($op1 < $op2));
            break;

            case 'more':
                $isError = (sizeof($op1) < sizeof($op2));
            break;
            case 'less':
                $isError = (sizeof($op1) > sizeof($op2));
            break;

            default:
                IError::fatal('validator.unknown_rule: '. $rule[2]);
        }// switch

        if ($isError)
            $this->addError('validator.rule.'.$rule[2], array(
                $this->fields[$rule[0]]['title'],
                $this->fields[$rule[1]]['title'])
            );
    }

    /** Constructs javascript for client-side validation
     * @param string $form name of HTML form to validate
     * @param boolean $isReturn sets if returned string contains 'return' statement
     * @param string $sDivName Id of HTML div for error output. if empty js uses alert()
     * @return string javascript code for validation
     */
    function makeJS($form = 'this', $isReturn=true, $divName='jsErr')
    {
        $script    = 'new Array( ';
        // fields
        foreach ($this->fields as $field => $scheme )
        {
            $script .= "{'field':'".$field."'";
            foreach ($scheme as  $k => $val)
            {
                if ($k == 'callback')
                    $val = $val[0];
                if (in_array($k, array('title','message')))
                    $val = "'".$val."'";
                $script .= ",'".$k."':".$val;
            }
            $script .= '},';
        }
        //rules
        $script = substr($script, 0, -1).'), new Array( ';
        foreach ($this->rules as $rule)
            $script .= sprintf("['%s','%s','%s','%s'],", $rule[0], $rule[1], $rule[2],
                _fmt('validator.rule.'.$rule[2], array($this->fields[$rule[0]]['title'], $this->fields[$rule[1]]['title'])));
        $script = substr($script, 0, -1).'),';

        //calbacks
        $script = substr($script, 0, -1).', new Array( ';
        foreach ($this->callback as $callback)
            $script .= $callback[1].",";
        $script = substr($script, 0, -1).')';

        $script =  " validator_isValid($form, $script, '$divName')";
        if ($isReturn)
            $script  = 'return '.$script;
        return $script;
    }

    /** Make javascript with messages array.
     * @return string javascript code (with <script> tags) for messages array
     */
    function makeJsMess()
    {
        require ('config/messages.php');
        $script = 'var aValMes = {};';
        foreach ($msg as $k=>$v)
        {
            $name = str_replace('validator.field.', '', $k);
            if ($name != $k)
                $script .= " aValMes['$name']  = '"._fmt('validator.field.'.$name, array('%s0', '%s1', '%s2', '%s3', '%s4'))."';";
        }
        return $script;
    }


    /** Validates single field.
     * @param mixed $mVal
     * @param array $scheme
     * @access private
     * @returns boolean
     */
    function _checkField($mValue, $scheme)
    {
        $isEmpty = ((is_string($mValue) and strlen($mValue)==0) or
                   (is_array($mValue)) and sizeof($mValue)==0);

        if ( isset($scheme['optional']) && $scheme['optional'] && $isEmpty)
            return true;

        $value = is_array($mValue)? $mValue : array($mValue);
        $err = array();

        // check size
        if ( isset($scheme['minsize']) and sizeof($value) < $scheme['minsize'])
            $err['minsize'] = array($scheme['title'], $scheme['minsize'], sizeof($value));

        if ( isset($scheme['maxsize']) and sizeof($value) > $scheme['maxsize'])
            $err['maxsize'] = array($scheme['title'], $scheme['maxsize'], sizeof($value));

        foreach($value as $val)
        {
            if ( isset($scheme['minlen']) and strlen($val) < $scheme['minlen'])
                $err['minlen'] = array($scheme['title'], $scheme['minlen'], strlen($val));

            if ( isset($scheme['maxlen']) and strlen($val) > $scheme['maxlen'])
                $err['maxlen'] =  array($scheme['title'], $scheme['maxlen'], strlen($val));

            if ( isset($scheme['pattern']) and !preg_match($scheme['pattern'], $val) )
                $err['pattern'] = array($scheme['title'], $scheme['pattern'], $val );

            if ( isset($scheme['min']) and $val < $scheme['min'])
                $err['min'] = array($scheme['title'], $scheme['min'], $val);

            if ( isset($scheme['max']) and $val > $scheme['max'])
                $err['max'] = array($scheme['title'], $scheme['max'], $val);

            if ( isset($scheme['mineq']) and $val <= $scheme['mineq'])
                $err['mineq'] = array($scheme['title'], $scheme['mineq'], $val);

            if ( isset($scheme['maxeq']) and $val >= $scheme['maxeq'])
                $err['maxeq'] = array($scheme['title'], $scheme['maxeq'], $val);
            //if ( isset($scheme['enum']) and !in_array($val, $scheme['enum']))
            //    $err['enum'] =  array($scheme['title'], $val);
            if (isset($scheme['callback']))
            {
                if (! call_user_func($scheme['callback'][1], $val))
                    $err['pattern'] = array($scheme['title'], $val);
            }
            if ($err) // find firs field with error and stop checks
                break;
        }

        if (! $err)
            return true;

        if (isset($scheme['message']))
            $this->addError('validator.field.custom', $scheme['message']);
        else
        {
            foreach ($err as $k=>$param)
                $this->addError('validator.field.'.$k, $param);
        }
        return false;
    }

    /** Gets default validation schemes by key. If unknown rule throws exeption
     * @access private
     * @param string $k
     * @return array scheme
     */
    function _getDef($k, $title = '')
    {
        $validatorDefs  = array(
            'email' => array(
                'pattern'=>'/^[0-9a-zA-Z]([\-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][\-0-9a-zA-Z]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,}$/',
                'maxlen'=>255,
            ),

            'ip' => array(
                'callback' => array('validator_isIp', array('Validator','isIp')),
                'pattern'  => '/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$/',
            ),
            'url' => array(
               'pattern' => '/^(?:(ftp|http|https):)?(?:\/\/(?:((?:%[0-9a-f]{2}|[\-a-z0-9_.!~*\'\(\);:&=\+\$,])+)@)?(?:((?:[a-z0-9](?:[\-a-z0-9]*[a-z0-9])?\.)*[a-z](?:[\-a-z0-9]*[a-z0-9])?)|([0-9]{1,3}(?:\.[0-9]{1,3}){3}))(?::([0-9]*))?)?((?:\/(?:%[0-9a-f]{2}|[\-a-z0-9_.!~*\'\(\):@&=\+\$,;])+)+)?\/?(?:\?.*)?$/i',
               'maxlen'=> 255,
               'minlen'=> 11
            ),

            'login' => array(
               'pattern'=>'/^[\w\-\.]+$/',
               'minlen'=>6,
               'maxlen'=>16
            ),
            'zip'  => array(
                'pattern'=>'/^[a-zA-Z\d\-\s]{1,10}$/',
            ),
            'zip_us'  => array(
                'pattern'=>'/^\d{5}(-\d{4})?$/',
            ),
            'zip_canada'  => array(
                'pattern'=>'/^[a-zA-Z\d]{6}$/',
            ),
            'phone_us'  => array(
                'minlen'=>12,
                'pattern'=>'/^([2-9]\d{2}\-\d{3}\-\d{4}|\([2-9]\d{2}\)\s?\d{3}\-\d{4})$/',
            ),
            'phone'  => array(
                'minlen'=> 1,
            ),
            'money' => array(
                'maxlen'=>12,
                'pattern'=>'/^\d+(\.\d\d)?$/',
            ),
            'cvv'  => array(
                'pattern'=>'/^\d{3,4}$/',
            ),
            'ssn'  => array(
                'maxlen' =>11,
                'pattern'=>'/^([\d]{3}-[\d]{2}-[\d]{4})|([\d]{2}-[\d]{7})$/',
            ),
            'integer' => array(
                'maxlen'=>20,
                'pattern'=>'/^\d+$/',
            ),
            'number' => array(
                'maxlen'=>20,
                'pattern'=>'/^\d+$/',
            ),            
            'float' => array(
                'maxlen'=>20,
                'pattern'=>'/^\d+(\.\d+)?$/',
            ),
            'cc_visa' => array(
                'maxlen'=>16,
                'pattern'=>'/^4(\d{12}|\d{15})$/',
            ),
            'cc_master' => array(
                'maxlen'=>16,
                'pattern'=>'/^5[1-5]\d{14}$/',
            ),
            'cc_ae' => array(
                'maxlen'=>15,
                'pattern'=>'/^3[47]\d{13}$/',
            ),
            'cc_discover' => array(
                'maxlen'=>16,
                'pattern'=>'/^6011\d{12}$/',
            ),
            'cc_diners' => array(
                'maxlen'=>14,
                'pattern'=>'/^3(0[0-5]|[68][0-9])\d{11}$/',
            ),
            'cc_jcb' => array(
                'maxlen'=>16,
                'pattern'=>'/^(3\d{4}|2131|1800)\d{11}$/',
            ),
            'cc_enroute' => array(
                'maxlen'=>15,
                'pattern'=>'/^2(014|149)\d{11}$/',
            ),
            'required'  => array(
                'minlen' =>1,
            ),
            'percent'   => array(
                'minlen'=>2,
                'pattern' => '/^([0-9]|[1-9][0-9]|100)$/'
            ),
            'bool'   => array(
                'pattern' => '/^[0-1]?$/'
            ),
        );
        if (! array_key_exists($k, $validatorDefs))
            IError::fatal('validator.unknown_def: '. $k);

        if (in_array($k, array('required', 'ssn', 'ip')))
            $validatorDefs[$k]['message'] = _fmt('validator.def.'.$k, array($title));

        return $validatorDefs[$k];
    }

    /** Checks if ip is in valid range
     * @param string $sIp
     * @return boolean result
     */
    function isIp($ip)
    {
        return ('0.0.0.0' != $ip && '255.255.255.255' != $ip);
    }
}
?>